#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>


typedef struct 
{
	int sock_conn;
	char ip[16];
	unsigned short port;

} client_info;



#pragma pack(push, 1)

typedef struct
{
	char name[100];      // 文件名
	uint64_t size;       // 文件大小

} file_info;

#pragma pack(pop)


int file_cnt = 0;               // 要发送的文件的数量
char* const * file_path = NULL;  // 要发送的文件的路径数组
file_info* fi = NULL;			// 要发送的文件的信息	


void* client_communication(void* arg);



int main(int argc, char** argv)
{
	if(argc < 2)
	{
		fprintf(stderr, "Usage: %s file_path1 [file_path2] ...\n", argv[0]);
		exit(EXIT_FAILURE);
	}

	file_path = &argv[1];
	file_cnt = argc - 1;

	fi = malloc(sizeof(file_info) * file_cnt);

	if(fi == NULL)
	{
		perror("malloc");
		exit(EXIT_FAILURE);
	}

	struct stat st;
	char* p = NULL;

	for(int i = 0; i < file_cnt; i++)
	{
		// 校验文件是否可读
		if(access(file_path[i], R_OK) == -1)
		{
			fprintf(stderr, "File %s is not readable!\n", file_path[i]);
			exit(EXIT_FAILURE);
		}

		// 获取文件长度
		if(lstat(file_path[i], &st) == -1)
		{
			fprintf(stderr, "lstat %s error!\n", file_path[i]);
			exit(EXIT_FAILURE);
		}

		fi[i].size = st.st_size;

		// 从文件路径中获取文件名
		p = strrchr(file_path[i], '/');

		if(p == NULL)
			strcpy(fi[i].name, file_path[i]);
		else
			strcpy(fi[i].name, p + 1);
	}

	// 忽略 SIGPIPE 信号
	signal(SIGPIPE, SIG_IGN);

	// 第 1 步：创建监听套接字
	int sock_listen = socket(AF_INET, SOCK_STREAM, 0);

	if(sock_listen == -1)
	{
		perror("socket");
		exit(EXIT_FAILURE);
	}

	// 开启地址复用，以便服务器快速重启
	int opt_val = 1;
	setsockopt(sock_listen, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof(opt_val));

	// 第 2 步：绑定地址
	struct sockaddr_in myaddr;
	myaddr.sin_family = AF_INET;                           // 指定地址家族为 Internet 地址家族
	//myaddr.sin_addr.s_addr = inet_addr("47.97.81.90");     // 指定要绑定的 IP 地址为本机的某个 IP 地址
	myaddr.sin_addr.s_addr = INADDR_ANY;                     // 指定要绑定的 IP 地址为本机任意 IP 地址
	myaddr.sin_port = htons(66); // 指定要绑定的端口号

	if(bind(sock_listen, (struct sockaddr*)&myaddr, sizeof(myaddr)) == -1)
	{
		perror("bind");
		exit(EXIT_FAILURE);
	}

	// 第 3 步：监听
	if(listen(sock_listen, 5) == -1)
	{
		perror("listen");
		exit(EXIT_FAILURE);
	}

	// 设置超时时间为10秒
	struct timeval tv;
	tv.tv_sec = 10;     // 多少秒
	tv.tv_usec = 0;     // 多少微秒

	pthread_t tid;
	struct sockaddr_in client_addr;           // 用于接收客户端地址信息
	socklen_t addr_len = sizeof(client_addr); // 用于接收客户端地址信息的长度
	client_info* ci = NULL;
	int sock_conn;


	while(1)
	{
		// 第 4 步：接受客户端连接请求
		sock_conn = accept(sock_listen, (struct sockaddr*)&client_addr, &addr_len);  // 如果对客户端地址感兴趣，后两个参数就不要传 NULL

		if(sock_conn == -1)
		{
			perror("accept");
			continue;
		}

		if (setsockopt(sock_conn, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) 
		{
			perror("setsockopt");
		}

		printf("\n客户端(%s:%hu)连接成功！\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

		// 创建新线程，负责与客户端通信
		ci = malloc(sizeof(client_info));

		if(ci == NULL)
		{
			perror("malloc");
			close(sock_conn);
			continue;
		}

		ci->sock_conn = sock_conn;
		strcpy(ci->ip, inet_ntoa(client_addr.sin_addr));
		ci->port = ntohs(client_addr.sin_port);

		if(pthread_create(&tid, NULL, client_communication, ci))
		{
			perror("pthread_create");
			close(sock_conn);
			free(ci);
			continue;
		}
	}

	// 第 7 步：关闭监听套接字
	close(sock_listen);

	return 0;
}



// 发送文件的线程函数
void* client_communication(void* arg)
{
	client_info* ci = arg;

	pthread_detach(pthread_self());


	// 第 5 步：收发数据


	for(int i = 0; i < file_cnt; i++)
	{
		// 发送文件信息
		send(ci->sock_conn, &fi[i], sizeof(file_info), 0);

		// 发送文件数据
		int fd = open(file_path[i], O_RDONLY);
		int ret;
		char buff[1024];

		while((ret = read(fd, buff, sizeof(buff))) > 0)
			if(write(ci->sock_conn, buff, ret) == -1)
				break;

		close(fd);
	}

	// 第 6 步：断开连接（关闭连接套接字，后面就不能再和这个连接套接字对应的客户端通信了）
	close(ci->sock_conn);
	printf("\n客户端(%s:%hu)断开连接！\n", ci->ip, ci->port);	

	free(ci);

	return NULL;
}
